/**
 * Copyright (c) 2012, David Varbel
 * All rights reserved.
 * 
 * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
 * 
 * 1) Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
 * 2) Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package com.epistrata.neutraltheory;

import java.util.ArrayList;
import java.util.List;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.Random;

import com.epistrata.experiment.Experiment;
import com.epistrata.experiment.Model;
import com.epistrata.experiment.ModelFactory;
import com.epistrata.experiment.Run;
import com.epistrata.experiment.View;

/**
 * @author David
 *
 */
public class NeutralTheoryModel implements Model {
	//Factors
	private double dispersalDistance;
	private double dispersalProbability;
	private double earlyDeathProbability;
	private int geneticDistance;
	private int minimumPossiblePotentialMates;
	private double mutationProbability;
	private int populationSize;
	private double spacialDistance;
	private int genomeSize;
	private double worldXSize;
	private double worldYSize;
	private int maxGens;
	
	//Internal model variables
	private Random random;
	private int generationNumber = 0;
	protected ArrayList<Critter> population;
	private Grid grid;
	private Run run;
	
	//Responses
	private int speciesCount = 0;
	private double sampleMean = 0;
	private double sampleVariance = 0;
	private ArrayList<Integer> speciesCountHistory = new ArrayList<Integer>();
	private final int sizeOfRecentHistory = 100;
	
	public NeutralTheoryModel(Run run) {
		if (run == null) {
			throw new IllegalArgumentException("A non-null Run must be provided.");
		}
		
		loadFactors(run.getFactors());
		this.run = run;
		
		random = new Random(run.getRandomSeed());
		
		//need to create grid in which to place critters
		grid = new Grid(this);
		
		//initial population is made of clones
		population = new ArrayList<Critter>();
		//create the first critter
		Critter firstRandomCritter = BitSetCritter.newRandomInstance(this);
		population.add(firstRandomCritter);
		//create the rest
		for (int critterNum = 1; critterNum < populationSize; critterNum++) {
			population.add(firstRandomCritter.clone());
		}

	}
	
	/**
	 * @param args Two required arguments are: [0] path to experiment config file; [1] path to experiment design file 
	 */
	public static void main(String[] args) {
		if (args.length < 2) {
			throw new IllegalArgumentException("Requires 2 command-line arguments: [0] path to experiment config file; [1] path to experiment design matrix file.");
		}
		
		ArrayList<View> customViews = new ArrayList<View>();
		customViews.add(new FileView());
		customViews.add(new PopulationImageView());
		
		Experiment experiment = new Experiment(args[0], args[1], customViews, 12);
		experiment.runExperiment(new ModelFactory() {
			public Model newInstance(Run run) {
				return new NeutralTheoryModel(run);
			}
		});
	}

	@Override
	public void run() {
		
		try {
			
			while (generationNumber < maxGens) {
				long genStart = System.nanoTime();
				
				evolve();
				
				long genFinish = System.nanoTime();
				run.message("Generation " + Integer.toString(generationNumber - 1) + " completed in " + Long.toString(genFinish - genStart) + " nanoseconds with " + Integer.toString(speciesCount) + " species.");

				//update views with any needed end-of-generation data
				run.dataUpdate(this);
			}
			
			//Calculate responses
			
			//recent mean
			sampleMean = 0;
			List<Integer> recentHistory = speciesCountHistory.subList(speciesCountHistory.size() - sizeOfRecentHistory, speciesCountHistory.size());
			for (int count : recentHistory) {
				sampleMean += count;
			}
			sampleMean /= (double) sizeOfRecentHistory;
			run.setResponse("mean", Double.toString(sampleMean));

			//recent variance
			sampleVariance = 0;
			for (int count : recentHistory) {
				sampleVariance += (count - sampleMean)*(count - sampleMean);
			}
			sampleVariance /= ((double) sizeOfRecentHistory) - 1.0d;
			run.setResponse("variance", Double.toString(sampleVariance));

		} catch(Exception e) {
			StringBuffer stackTrace = new StringBuffer();
			for(StackTraceElement element : e.getStackTrace()) {
				stackTrace.append("\t" + element.toString() + "\n");
			}
			run.message("Run " + Integer.toString(run.getRunNumber()) + " ended early in generation " + Integer.toString(generationNumber ) +  " due to exception: " + e + "\n" + stackTrace);
		}
		
	}

	private void loadFactors(HashMap<String, String> factors) {
		try {
			//dispersalDistance = Double.parseDouble(factors.get("dispersal.distance"));
			//dispersalProbability = Double.parseDouble(factors.get("dispersal.probability"));
			dispersalDistance = 7.0d;
			dispersalProbability = 0.01d;
			earlyDeathProbability = Double.parseDouble(factors.get("early.death.probability"));
			geneticDistance = Integer.parseInt(factors.get("genetic.distance"));
			minimumPossiblePotentialMates = Integer.parseInt(factors.get("minimum.possible.potential.mates"));
			mutationProbability = Double.parseDouble(factors.get("mutation.probability"));
			spacialDistance = Double.parseDouble(factors.get("spacial.distance"));
			populationSize = 2000;
			genomeSize = 125;
			worldXSize = 128.0;
			worldYSize = 128.0;
			maxGens = 4000;
		} catch (NumberFormatException e) {
			throw new NumberFormatException("One of the factors was not formatted correctly.");
		} catch (NullPointerException e) {
			throw new NullPointerException("One of the required factors was not provided to the model or was named incorrectly.  Factors provided:\n" + factors.toString());
		}
	}

	private void evolve() {
		assert population.size() == populationSize;
		assert grid.getTotalCritterCount() == populationSize;
		
		
		//loop handles selection of mating pairs, but crossover and mutation are handled by Critter
		int currentCritterNum = 0;
		for (Critter currentCritter : population) {

			boolean earlyDeath = false;
			Critter earlyDeathCritter = null;
			if (random.nextDouble() < earlyDeathProbability) {
				earlyDeath = true;
				
				//TODO somehow using the "early death" functionality causes species to rapidly form in 45deg bands.  They tend to move down and to the right as they evolve.
				//pick another critter in the vicinity
				double searchRadius = spacialDistance;
				LinkedList<Critter> possibleOpportunists = new LinkedList<Critter>();
				while (possibleOpportunists.size() < 1) {
					possibleOpportunists = grid.getCrittersInNeighborhood(currentCritter, searchRadius);
					searchRadius += spacialDistance;
				}
				earlyDeathCritter = currentCritter;
				//opportunist parent replaces current critter reference 
				currentCritter = possibleOpportunists.get(random.nextInt(possibleOpportunists.size()));
			}
			

			//limit search for mates to critters in spacial neighborhood
			LinkedList<Critter> possibleMates = new LinkedList<Critter>();
			int maxGeneticDistance = geneticDistance;
			double maxSpacialDistance = spacialDistance;
			do {
				LinkedList<Critter> neighborCritters = grid.getCrittersInNeighborhood(currentCritter, maxSpacialDistance);
				for (Critter otherCritter : neighborCritters) {
					
					//can't mate with itself
					if (otherCritter == currentCritter) {
						continue;
					}
	
					/*
					 * Possible mate must satisfy 2 criteria:
					 * 1) it must be the same species
					 * 2) it must be within the spacial maximum mating distance
					 */
					if (currentCritter.isSameSpecies(otherCritter, maxGeneticDistance) &&
							currentCritter.isWithinMatingDistance(otherCritter, maxSpacialDistance)) {
						
						possibleMates.add(otherCritter);
					}
				}
	
				//if no mates found, broaden the criteria until a mate is found
				maxGeneticDistance += 1;
				maxSpacialDistance += 1.0d;
			} while (possibleMates.size() < minimumPossiblePotentialMates);
			
			//among possible mates randomly choose with which one to mate
			int mateIndex = random.nextInt(possibleMates.size());
			//offspring replaces parent unless parent died early
			if (!earlyDeath) {
				population.set(currentCritterNum, currentCritter.mateWith(possibleMates.get(mateIndex)));
			} else {  //if original occupant of space died early, place extra offspring of opportunist in vacant space
				population.set(currentCritterNum, currentCritter.mateOpportunistWith(possibleMates.get(mateIndex), earlyDeathCritter));
			}
			
			currentCritterNum++;	
		}
		
		//perform end of generation calculations
		updateSpeciesCount();
		speciesCountHistory.add(speciesCount);
		generationNumber++;
		
	}
	
	/**
 	 * Count the number of species in the current population.
 	 * 
 	 * We can have "ring" species, in which progressive
	 * differences along a chain of individuals result in individuals at one end
	 * not being able to mate with those at the other end.
	 * 
	 */
	public void updateSpeciesCount() {
		/*
		 * Start with first critter, assign species index
		 * Check every other critter for match
		 * 	if match, assign them the same index
		 * For each critter that matched, check every other unmatched critter for match
		 *  if match, assign them the same index
		 */
		int speciesIndex = 0;
		for (Critter critter : population) {
			if (critter.getSpeciesIndex() == -1) {
				
				critter.setSpeciesIndex(speciesIndex);
				int unassignedCritters = assignSpeciesIndexToRelated(critter, speciesIndex);
				
				speciesIndex++;
				
				//shortcut: if there are no more critters that haven't been assigned a species, exit loop early
				if (unassignedCritters == 0) {
					break;
				}
			}
		}
		
		this.speciesCount = speciesIndex;
	}
	
	public int getSpeciesCount() {
		return speciesCount;
	}
	
	private int assignSpeciesIndexToRelated(Critter critter, int speciesIndex) {
		int unassignedCritters = 0;
		ArrayList<Critter> relatedCritters = new ArrayList<Critter>();
		//check all unassigned critters against this reference critter
		//for critters that are the same species, keep a list of them, and then recursively check each of these for others of the same species
		for (Critter otherCritter : population) {
			if (otherCritter.getSpeciesIndex() == -1) {
				unassignedCritters++;
				//compare other genome to reference genome to see if same species
				if (critter.isSameSpecies(otherCritter, geneticDistance)) {
					otherCritter.setSpeciesIndex(speciesIndex);
					relatedCritters.add(otherCritter);
					unassignedCritters--;
				}
			}
		}
		if (relatedCritters.size() > 0 && unassignedCritters > 0) {
			for (Critter relation : relatedCritters) {
				unassignedCritters = assignSpeciesIndexToRelated(relation, speciesIndex);
				//check for early out, no need to keep checking if all critters have an assigned species
				if (unassignedCritters == 0) {
					break;
				}
			}
		}
		return unassignedCritters;
	}

	
	Random getRandom() {
		return random;
	}

	public double getDispersalDistance() {
		return dispersalDistance;
	}

	public double getDispersalProbability() {
		return dispersalProbability;
	}

	public double getEarlyDeathProbability() {
		return earlyDeathProbability;
	}

	public int getGeneticDistance() {
		return geneticDistance;
	}

	public int getMinimumPossiblePotentialMates() {
		return minimumPossiblePotentialMates;
	}

	public double getMutationProbability() {
		return mutationProbability;
	}

	public int getPopulationSize() {
		return populationSize;
	}

	public double getSpacialDistance() {
		return spacialDistance;
	}

	public int getGenomeSize() {
		return genomeSize;
	}

	public double getWorldXSize() {
		return worldXSize;
	}

	public double getWorldYSize() {
		return worldYSize;
	}

	public int getGenerationNumber() {
		return generationNumber;
	}
	
	Grid getGrid() {
		return grid;
	}
	
	public int getMaxGens() {
		return maxGens;
	}
	
	List<Integer> getSpeciesCountHistory() {
		return speciesCountHistory;
	}
	
	public List<Critter> getPopulation() {
		return population; //a little dangerous, but speed is important when this call is made regularly
	}

}
